package cn.iocoder.yudao.module.infra.controller.admin.codegen;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
import cn.iocoder.yudao.module.system.dal.mysql.permission.MenuMapper;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.Paragraphs;
import com.deepoove.poi.data.style.Style;
import com.deepoove.poi.policy.ParagraphRenderPolicy;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.CODEGEN_SUB_COLUMN_NOT_EXISTS;
import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment;

@Slf4j
@Tag(name = "管理后台 - 代码生成器 - 导出到文档")
@RestController
@RequestMapping("/infra/codegen")
@Validated
public class CodegenDocController {

    @Resource
    private MenuMapper menuMapper;
    @Resource
    private CodegenTableMapper codegenTableMapper;
    @Resource
    private CodegenColumnMapper codegenColumnMapper;
    @Resource
    private CodegenEngine codegenEngine;
    @Resource
    private ApplicationContext applicationContext;

    @Operation(summary = "生成代码并导出到word文档")
    @GetMapping("/export-code-as-word-doc")
    @PreAuthorize("@ss.hasPermission('infra:codegen:export')")
    public void exportCodeAsWordDoc(@RequestParam("tableIds") String tableIds,
                                    @RequestParam("systemName") String systemName,
                                    @RequestParam("systemOverview") String systemOverview,
                                    @RequestParam("codeCount") String codeCount,
                                    @RequestParam("purpose") String purpose,
                                    @RequestParam("functions") String functions,
                                    HttpServletResponse response) throws IOException {
        System.out.println("tableIds = " + tableIds);
        List<Long> tablesIdss = Arrays.stream(tableIds.split(",")).map(Long::valueOf).toList();
        Configure config = Configure.builder()
                .bind("code", new ParagraphRenderPolicy())
                .build();
        Map<String, Object> model = new HashMap<>();
        model.put("SYSTEMNAME", systemName);
        model.put("systemOverview", systemOverview); // 系统概述
        model.put("codeCount", codeCount);
        model.put("purpose", purpose);
        model.put("function", functions);
        model.put("code", "");
        for (int i = 0; i < tablesIdss.size(); i++) {
            Long tableId = tablesIdss.get(i);
            int finalI = i + 1;
            Map<String, String> generatedCode = generationCodesDoc(tableId,
                    (templateName) -> Strings.CS.containsAny(templateName, "sql.vm", "mapper.xml.vm", "errorcode.vm"),
                    (velocityContext, table) -> {
                        MenuDO sysMenu = menuMapper.selectOne(MenuDO::getId, table.getParentMenuId());
                        if (sysMenu != null) {
                            model.put("systemCode", sysMenu.getPath().replace("/", ""));
                            model.put("ROOTMENU", sysMenu.getName());
                        }
                        model.put("MENU" + finalI, table.getTableComment().replace("表", ""));
                        model.put(String.format("menu%s_code", finalI), table.getBusinessName());
                        model.put("tablename_" + finalI, table.getTableComment().replace("表", ""));
                        model.put("tablename_english_" + finalI, table.getTableName());
                        int j = 1;
                        List<CodegenColumnDO> columns = (List<CodegenColumnDO>) velocityContext.get("columns");
                        CodegenColumnDO primaryColumn = null;
                        for (CodegenColumnDO genTableColumn : columns) {
                            if (genTableColumn.getPrimaryKey()) {
                                primaryColumn = genTableColumn;
                                continue;
                            } else {
                                model.put(String.format("menu%s_column%s", finalI, j), genTableColumn.getColumnComment());
                            }
                            j++;
                        }

                        velocityContext.put("menu_order_num", finalI);
                        try {
                            // ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service
                            Class<?> service = Class.forName(String.format("%s.module.%s.service.%s.%sService",
                                    velocityContext.get("basePackage"), table.getModuleName(), table.getBusinessName(), table.getClassName()));
////                            ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO
//                            ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.${table.className}PageReqVO
                            Class<?> domain = Class.forName(String.format("%s.module.%s.controller.admin.%s.vo.%sPageReqVO",
                                    velocityContext.get("basePackage"), table.getModuleName(), table.getBusinessName(), table.getClassName()));
                            Object serviceBean = applicationContext.getBean(service);
                            Method method = service.getMethod(String.format("get%sPage", velocityContext.get("simpleClassName")), domain);
                            PageResult<Object> list = (PageResult<Object>) method.invoke(serviceBean, domain.newInstance());
                            SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
                            filter.getExcludes().add("params");
                            String jsonString = JSONUtil.toJsonPrettyStr(list);
                            jsonString = jsonString.replaceAll("\t", "  ");
                            jsonString = jsonString.replaceAll("\n", "\n    ");
                            jsonString = jsonString.replaceAll(":", ": ");
                            model.put(String.format("menu%s_list_response", finalI), jsonString);
                            String request = JSONUtil.toJsonPrettyStr(list.getList().get(0));
                            request = request.replaceAll("\t", "  ");
                            request = request.replaceAll("\n", "\n    ");
                            request = request.replaceAll(":", ": ");
                            model.put(String.format("menu%s_update_request", finalI), request);
////                            filter.getExcludes().add("id");
                            request = JSONUtil.toJsonPrettyStr(list.getList().get(0));
                            request = request.replaceAll("\t", "  ");
                            request = request.replaceAll("\n", "\n    ");
                            if (primaryColumn != null) {
                                request = request.replace("\"" + primaryColumn.getColumnName() + "\":1", "\"" + primaryColumn.getColumnName() + "\":null");
                            }
                            request = request.replaceAll(":", ": ");
                            model.put(String.format("menu%s_add_request", finalI), request);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    });
            for (Map.Entry<String, String> entry : generatedCode.entrySet()) {
                if (entry.getKey().endsWith(".vue") && entry.getValue().contains("<template>")) {
                    entry.setValue(entry.getValue().substring(entry.getValue().indexOf("<template>")));
                }
            }
            model.put("code", StringUtils.isEmpty((String) model.get("code")) ?
                    String.join("\n", generatedCode.values()) :
                    model.get("code") + "\n" + String.join("\n", generatedCode.values()));
        }
        Paragraphs.ParagraphBuilder paragraphBuilder = Paragraphs.of();
        paragraphBuilder.addText(((String) model.get("code")).replaceAll("yudao.", ""))
                .left()
                .defaultTextStyle(Style.builder().buildFontFamily("宋体").build());
        model.put("code", paragraphBuilder.create());

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipOutputStream zip = new ZipOutputStream(outputStream);
        try (XWPFTemplate template = XWPFTemplate.compile(codeTemplateFile.getFile().getAbsolutePath(), config).render(model)) {
            try { // 添加到zip
                zip.putNextEntry(new ZipEntry(String.format("%s%s%s-代码.%s", systemName, File.separator, systemName,
                        designSpecTemplateFile.getFile().getAbsolutePath().substring(designSpecTemplateFile.getFile().getAbsolutePath().lastIndexOf(".") + 1))));
                template.write(zip);
                zip.flush();
                zip.closeEntry();
            } catch (IOException e) {
                log.error("渲染 code 模板失败", e);
            }
        } catch (IOException e) {
            // TODO
            throw new RuntimeException(e);
        }

        try (XWPFTemplate template = XWPFTemplate.compile(designSpecTemplateFile.getFile().getAbsolutePath(), config).render(model)) {
            try { // 添加到zip
                zip.putNextEntry(new ZipEntry(String.format("%s%s%s-设计说明书.%s", systemName, File.separator, systemName,
                        designSpecTemplateFile.getFile().getAbsolutePath().substring(designSpecTemplateFile.getFile().getAbsolutePath().lastIndexOf(".") + 1))));
                template.write(zip);
                zip.flush();
                zip.closeEntry();
            } catch (IOException e) {
                log.error("渲染设计说明书模板失败", e);
            }
        } catch (IOException e) {
            // TODO
            throw new RuntimeException(e);
        }

        try (XWPFTemplate template = XWPFTemplate.compile(informationTemplateFile.getFile().getAbsolutePath(), config).render(model)) {
            try { // 添加到zip
                zip.putNextEntry(new ZipEntry(String.format("%s%s%s-信息采集表.%s", systemName, File.separator, systemName,
                        informationTemplateFile.getFile().getAbsolutePath().substring(informationTemplateFile.getFile().getAbsolutePath().lastIndexOf(".") + 1))));
                template.write(zip);
                zip.flush();
                zip.closeEntry();
            } catch (IOException e) {
                log.error("渲染设计说明书模板失败", e);
            }
        } catch (IOException e) {
            // TODO
            throw new RuntimeException(e);
        }
        IOUtils.closeQuietly(zip);
//        // 输出
        writeAttachment(response, "codegen-docs.zip", outputStream.toByteArray());
    }

    @Value("classpath:templates/${template.code:XXX-代码.docx}")
    org.springframework.core.io.Resource codeTemplateFile;

    @Value("classpath:templates/${template.manual:XXX-设计说明书.docx}")
    org.springframework.core.io.Resource designSpecTemplateFile;

    @Value("classpath:templates/${template.information:XXX-信息采集表.docx}")
    org.springframework.core.io.Resource informationTemplateFile;

    public Map<String, String> generationCodesDoc(Long tableId, Function<String, Boolean> templateExclude, BiConsumer<Map<String, Object>, CodegenTableDO> velocityContextConsumer) {
        // 校验是否已经存在
        CodegenTableDO table = codegenTableMapper.selectById(tableId);
        if (table == null) {
            throw exception(CODEGEN_TABLE_NOT_EXISTS);
        }
        List<CodegenColumnDO> columns = codegenColumnMapper.selectListByTableId(tableId);
        if (CollUtil.isEmpty(columns)) {
            throw exception(CODEGEN_COLUMN_NOT_EXISTS);
        }

        // 如果是主子表，则加载对应的子表信息
        List<CodegenTableDO> subTables;
        List<List<CodegenColumnDO>> subColumnsList = null;
        if (CodegenTemplateTypeEnum.isMaster(table.getTemplateType())) {
            // 校验子表存在
            subTables = codegenTableMapper.selectListByTemplateTypeAndMasterTableId(
                    CodegenTemplateTypeEnum.SUB.getType(), tableId);
            if (CollUtil.isEmpty(subTables)) {
                throw exception(CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE);
            }
            // 校验子表的关联字段存在
            subColumnsList = new ArrayList<>();
            for (CodegenTableDO subTable : subTables) {
                List<CodegenColumnDO> subColumns = codegenColumnMapper.selectListByTableId(subTable.getId());
                if (CollUtil.findOne(subColumns, column -> column.getId().equals(subTable.getSubJoinColumnId())) == null) {
                    throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, subTable.getId());
                }
                subColumnsList.add(subColumns);
            }
        } else {
            subTables = null;
        }
        // 1.1 初始化 bindMap 上下文
        Map<String, Object> bindingMap = codegenEngine.initBindingMap(table, columns, subTables, subColumnsList);
        // 1.2 获得模版
        Map<String, String> templates = codegenEngine.getTemplates(table.getFrontType());

        // 2. 执行生成
        Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
        templates.forEach((vmPath, filePath) -> {
            if (templateExclude.apply(vmPath)) {
                return;
            }
            // 2.1 特殊：主子表专属逻辑
            if (CodegenEngine.isSubTemplate(vmPath)) {
                codegenEngine.generateSubCode(table, subTables, result, vmPath, filePath, bindingMap);
                return;
                // 2.2 特殊：树表专属逻辑
            } else if (CodegenEngine.isPageReqVOTemplate(vmPath)) {
                // 减少多余的类生成，例如说 PageVO.java 类
                if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
                    return;
                }
            } else if (CodegenEngine.isListReqVOTemplate(vmPath)) {
                // 减少多余的类生成，例如说 ListVO.java 类
                if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
                    return;
                }
            }
            // 2.3 默认生成
            codegenEngine.generateCode(result, vmPath, filePath, bindingMap);
        });
        velocityContextConsumer.accept(bindingMap, table);
        return result;
    }

}
